/** @preserve
 * jsPDF addImage plugin
 * Copyright (c) 2012 Jason Siefken, https://github.com/siefkenj/
 *               2013 Chris Dowling, https://github.com/gingerchris
 *               2013 Trinh Ho, https://github.com/ineedfat
 *               2013 Edwin Alejandro Perez, https://github.com/eaparango
 *               2013 Norah Smith, https://github.com/burnburnrocket
 *               2014 Diego Casorran, https://github.com/diegocr
 *               2014 James Robb, https://github.com/jamesbrobb
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

;(function(jsPDFAPI) {
    'use strict'

    var namespace = 'addImage_',
        supported_image_types = ['jpeg', 'jpg', 'png'];

    // Image functionality ported from pdf.js
    var putImage = function(img) {

        var objectNumber = this.internal.newObject()
            , out = this.internal.write
            , putStream = this.internal.putStream

        img['n'] = objectNumber

        out('<</Type /XObject')
        out('/Subtype /Image')
        out('/Width ' + img['w'])
        out('/Height ' + img['h'])
        if (img['cs'] === this.color_spaces.INDEXED) {
            out('/ColorSpace [/Indexed /DeviceRGB '
                // if an indexed png defines more than one colour with transparency, we've created a smask
                + (img['pal'].length / 3 - 1) + ' ' + ('smask' in img ? objectNumber + 2 : objectNumber + 1)
                + ' 0 R]');
        } else {
            out('/ColorSpace /' + img['cs']);
            if (img['cs'] === this.color_spaces.DEVICE_CMYK) {
                out('/Decode [1 0 1 0 1 0 1 0]');
            }
        }
        out('/BitsPerComponent ' + img['bpc']);
        if ('f' in img) {
            out('/Filter /' + img['f']);
        }
        if ('dp' in img) {
            out('/DecodeParms <<' + img['dp'] + '>>');
        }
        if ('trns' in img && img['trns'].constructor == Array) {
            var trns = '',
                i = 0,
                len = img['trns'].length;
            for (; i < len; i++)
                trns += (img['trns'][i] + ' ' + img['trns'][i] + ' ');
            out('/Mask [' + trns + ']');
        }
        if ('smask' in img) {
            out('/SMask ' + (objectNumber + 1) + ' 0 R');
        }
        out('/Length ' + img['data'].length + '>>');

        putStream(img['data']);

        out('endobj');

        // Soft mask
        if ('smask' in img) {
            var dp = '/Predictor 15 /Colors 1 /BitsPerComponent ' + img['bpc'] + ' /Columns ' + img['w'];
            var smask = {'w': img['w'], 'h': img['h'], 'cs': 'DeviceGray', 'bpc': img['bpc'], 'dp': dp, 'data': img['smask']};
            if ('f' in img)
                smask.f = img['f'];
            putImage.call(this, smask);
        }

        //Palette
        if (img['cs'] === this.color_spaces.INDEXED) {

            this.internal.newObject();
            //out('<< /Filter / ' + img['f'] +' /Length ' + img['pal'].length + '>>');
            //putStream(zlib.compress(img['pal']));
            out('<< /Length ' + img['pal'].length + '>>');
            putStream(this.arrayBufferToBinaryString(new Uint8Array(img['pal'])));
            out('endobj');
        }
    }
        , putResourcesCallback = function() {
        var images = this.internal.collections[namespace + 'images']
        for ( var i in images ) {
            putImage.call(this, images[i])
        }
    }
        , putXObjectsDictCallback = function(){
        var images = this.internal.collections[namespace + 'images']
            , out = this.internal.write
            , image
        for (var i in images) {
            image = images[i]
            out(
                '/I' + image['i']
                , image['n']
                , '0'
                , 'R'
            )
        }
    }
        , checkCompressValue = function(value) {
        if(value && typeof value === 'string')
            value = value.toUpperCase();
        return value in jsPDFAPI.image_compression ? value : jsPDFAPI.image_compression.NONE;
    }
        , getImages = function() {
        var images = this.internal.collections[namespace + 'images'];
        //first run, so initialise stuff
        if(!images) {
            this.internal.collections[namespace + 'images'] = images = {};
            this.internal.events.subscribe('putResources', putResourcesCallback);
            this.internal.events.subscribe('putXobjectDict', putXObjectsDictCallback);
        }

        return images;
    }
        , getImageIndex = function(images) {
        var imageIndex = 0;

        if (images){
            // this is NOT the first time this method is ran on this instance of jsPDF object.
            imageIndex = Object.keys ?
                Object.keys(images).length :
                (function(o){
                    var i = 0
                    for (var e in o){if(o.hasOwnProperty(e)){ i++ }}
                    return i
                })(images)
        }

        return imageIndex;
    }
        , notDefined = function(value) {
        return typeof value === 'undefined' || value === null;
    }
        , generateAliasFromData = function(data) {
        // TODO: Alias dynamic generation from imageData's checksum/hash
        return undefined;
    }
        , doesNotSupportImageType = function(type) {
        return supported_image_types.indexOf(type) === -1;
    }
        , processMethodNotEnabled = function(type) {
        return typeof jsPDFAPI['process' + type.toUpperCase()] !== 'function';
    }
        , isDOMElement = function(object) {
        return typeof object === 'object' && object.nodeType === 1;
    }
        , createDataURIFromElement = function(element, format) {

        //if element is an image which uses data url defintion, just return the dataurl
        if (element.nodeName === 'IMG' && element.hasAttribute('src')) {
            var src = ''+element.getAttribute('src');
            if (src.indexOf('data:image/') === 0) return src;

            // only if the user doesn't care about a format
            if (!format && /\.png(?:[?#].*)?$/i.test(src)) format = 'png';
        }

        if(element.nodeName === 'CANVAS') {
            var canvas = element;
        } else {
            var canvas = document.createElement('canvas');
            canvas.width = element.clientWidth || element.width;
            canvas.height = element.clientHeight || element.height;

            var ctx = canvas.getContext('2d');
            if (!ctx) {
                throw ('addImage requires canvas to be supported by browser.');
            }
            ctx.drawImage(element, 0, 0, canvas.width, canvas.height);
        }

        return canvas.toDataURL((''+format).toLowerCase() == 'png' ? 'image/png' : 'image/jpeg');
    }
        ,checkImagesForAlias = function(imageData, images) {
        var cached_info;
        if(images) {
            for(var e in images) {
                if(imageData === images[e].alias) {
                    cached_info = images[e];
                    break;
                }
            }
        }
        return cached_info;
    }
        ,determineWidthAndHeight = function(w, h, info) {
        if (!w && !h) {
            w = -96;
            h = -96;
        }
        if (w < 0) {
            w = (-1) * info['w'] * 72 / w / this.internal.scaleFactor;
        }
        if (h < 0) {
            h = (-1) * info['h'] * 72 / h / this.internal.scaleFactor;
        }
        if (w === 0) {
            w = h * info['w'] / info['h'];
        }
        if (h === 0) {
            h = w * info['h'] / info['w'];
        }

        return [w, h];
    }
        , writeImageToPDF = function(x, y, w, h, info, index, images) {
        var dims = determineWidthAndHeight.call(this, w, h, info),
            coord = this.internal.getCoordinateString,
            vcoord = this.internal.getVerticalCoordinateString;

        w = dims[0];
        h = dims[1];

        images[index] = info;

        this.internal.write(
            'q'
            , coord(w)
            , '0 0'
            , coord(h) // TODO: check if this should be shifted by vcoord
            , coord(x)
            , vcoord(y + h)
            , 'cm /I'+info['i']
            , 'Do Q'
        )
    };

    /**
     * COLOR SPACES
     */
    jsPDFAPI.color_spaces = {
        DEVICE_RGB:'DeviceRGB',
        DEVICE_GRAY:'DeviceGray',
        DEVICE_CMYK:'DeviceCMYK',
        CAL_GREY:'CalGray',
        CAL_RGB:'CalRGB',
        LAB:'Lab',
        ICC_BASED:'ICCBased',
        INDEXED:'Indexed',
        PATTERN:'Pattern',
        SEPERATION:'Seperation',
        DEVICE_N:'DeviceN'
    };

    /**
     * DECODE METHODS
     */
    jsPDFAPI.decode = {
        DCT_DECODE:'DCTDecode',
        FLATE_DECODE:'FlateDecode',
        LZW_DECODE:'LZWDecode',
        JPX_DECODE:'JPXDecode',
        JBIG2_DECODE:'JBIG2Decode',
        ASCII85_DECODE:'ASCII85Decode',
        ASCII_HEX_DECODE:'ASCIIHexDecode',
        RUN_LENGTH_DECODE:'RunLengthDecode',
        CCITT_FAX_DECODE:'CCITTFaxDecode'
    };

    /**
     * IMAGE COMPRESSION TYPES
     */
    jsPDFAPI.image_compression = {
        NONE: 'NONE',
        FAST: 'FAST',
        MEDIUM: 'MEDIUM',
        SLOW: 'SLOW'
    };

    jsPDFAPI.isString = function(object) {
        return typeof object === 'string';
    };

    /**
     * Strips out and returns info from a valid base64 data URI
     * @param {String[dataURI]} a valid data URI of format 'data:[<MIME-type>][;base64],<data>'
     * @returns an Array containing the following
     * [0] the complete data URI
     * [1] <MIME-type>
     * [2] format - the second part of the mime-type i.e 'png' in 'image/png'
     * [4] <data>
     */
    jsPDFAPI.extractInfoFromBase64DataURI = function(dataURI) {
        return /^data:([\w]+?\/([\w]+?));base64,(.+?)$/g.exec(dataURI);
    };

    /**
     * Check to see if ArrayBuffer is supported
     */
    jsPDFAPI.supportsArrayBuffer = function() {
        return typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined';
    };

    /**
     * Tests supplied object to determine if ArrayBuffer
     * @param {Object[object]}
     */
    jsPDFAPI.isArrayBuffer = function(object) {
        if(!this.supportsArrayBuffer())
            return false;
        return object instanceof ArrayBuffer;
    };

    /**
     * Tests supplied object to determine if it implements the ArrayBufferView (TypedArray) interface
     * @param {Object[object]}
     */
    jsPDFAPI.isArrayBufferView = function(object) {
        if(!this.supportsArrayBuffer())
            return false;
        if(typeof Uint32Array === 'undefined')
            return false;
        return (object instanceof Int8Array ||
            object instanceof Uint8Array ||
            (typeof Uint8ClampedArray !== 'undefined' && object instanceof Uint8ClampedArray) ||
            object instanceof Int16Array ||
            object instanceof Uint16Array ||
            object instanceof Int32Array ||
            object instanceof Uint32Array ||
            object instanceof Float32Array ||
            object instanceof Float64Array );
    };

    /**
     * Exactly what it says on the tin
     */
    jsPDFAPI.binaryStringToUint8Array = function(binary_string) {
        /*
         * not sure how efficient this will be will bigger files. Is there a native method?
         */
        var len = binary_string.length;
        var bytes = new Uint8Array( len );
        for (var i = 0; i < len; i++) {
            bytes[i] = binary_string.charCodeAt(i);
        }
        return bytes;
    };

    /**
     * @see this discussion
     * http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers
     *
     * As stated, i imagine the method below is highly inefficent for large files.
     *
     * Also of note from Mozilla,
     *
     * "However, this is slow and error-prone, due to the need for multiple conversions (especially if the binary data is not actually byte-format data, but, for example, 32-bit integers or floats)."
     *
     * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView
     *
     * Although i'm strugglig to see how StringView solves this issue? Doesn't appear to be a direct method for conversion?
     *
     * Async method using Blob and FileReader could be best, but i'm not sure how to fit it into the flow?
     */
    jsPDFAPI.arrayBufferToBinaryString = function(buffer) {
        if(this.isArrayBuffer(buffer))
            buffer = new Uint8Array(buffer);

        var binary_string = '';
        var len = buffer.byteLength;
        for (var i = 0; i < len; i++) {
            binary_string += String.fromCharCode(buffer[i]);
        }
        return binary_string;
        /*
         * Another solution is the method below - convert array buffer straight to base64 and then use atob
         */
        //return atob(this.arrayBufferToBase64(buffer));
    };

    /**
     * Converts an ArrayBuffer directly to base64
     *
     * Taken from here
     *
     * http://jsperf.com/encoding-xhr-image-data/31
     *
     * Need to test if this is a better solution for larger files
     *
     */
    jsPDFAPI.arrayBufferToBase64 = function(arrayBuffer) {
        var base64    = ''
        var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

        var bytes         = new Uint8Array(arrayBuffer)
        var byteLength    = bytes.byteLength
        var byteRemainder = byteLength % 3
        var mainLength    = byteLength - byteRemainder

        var a, b, c, d
        var chunk

        // Main loop deals with bytes in chunks of 3
        for (var i = 0; i < mainLength; i = i + 3) {
            // Combine the three bytes into a single integer
            chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]

            // Use bitmasks to extract 6-bit segments from the triplet
            a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
            b = (chunk & 258048)   >> 12 // 258048   = (2^6 - 1) << 12
            c = (chunk & 4032)     >>  6 // 4032     = (2^6 - 1) << 6
            d = chunk & 63               // 63       = 2^6 - 1

            // Convert the raw binary segments to the appropriate ASCII encoding
            base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
        }

        // Deal with the remaining bytes and padding
        if (byteRemainder == 1) {
            chunk = bytes[mainLength]

            a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2

            // Set the 4 least significant bits to zero
            b = (chunk & 3)   << 4 // 3   = 2^2 - 1

            base64 += encodings[a] + encodings[b] + '=='
        } else if (byteRemainder == 2) {
            chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]

            a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
            b = (chunk & 1008)  >>  4 // 1008  = (2^6 - 1) << 4

            // Set the 2 least significant bits to zero
            c = (chunk & 15)    <<  2 // 15    = 2^4 - 1

            base64 += encodings[a] + encodings[b] + encodings[c] + '='
        }

        return base64
    };

    jsPDFAPI.createImageInfo = function(data, wd, ht, cs, bpc, f, imageIndex, alias, dp, trns, pal, smask) {
        var info = {
            alias:alias,
            w : wd,
            h : ht,
            cs : cs,
            bpc : bpc,
            i : imageIndex,
            data : data
            // n: objectNumber will be added by putImage code
        };

        if(f) info.f = f;
        if(dp) info.dp = dp;
        if(trns) info.trns = trns;
        if(pal) info.pal = pal;
        if(smask) info.smask = smask;

        return info;
    };

    jsPDFAPI.addImage = function(imageData, format, x, y, w, h, alias, compression) {
        'use strict'

        if(typeof format === 'number') {
            var tmp = h;
            h = w;
            w = y;
            y = x;
            x = format;
            format = tmp;
        }

        var images = getImages.call(this),//initalises internals and events on first run
            dataAsBinaryString;

        compression = checkCompressValue(compression);

        if(notDefined(alias))
            alias = generateAliasFromData(imageData);

        if(isDOMElement(imageData))
            imageData = createDataURIFromElement(imageData, format);

        if(this.isString(imageData)) {

            var base64Info = this.extractInfoFromBase64DataURI(imageData);

            if(base64Info) {

                format = base64Info[2];
                imageData = atob(base64Info[3]);//convert to binary string

            } else {

                if (imgData.charCodeAt(0) === 0x89 &&
                    imgData.charCodeAt(1) === 0x50 &&
                    imgData.charCodeAt(2) === 0x4e &&
                    imgData.charCodeAt(3) === 0x47  )  format = 'png';
            }
        }
        format = (format || 'JPEG').toLowerCase();

        if(doesNotSupportImageType(format))
            throw new Error('addImage currently only supports formats ' + supported_image_types + ', not \''+format+'\'');

        if(processMethodNotEnabled(format))
            throw new Error('please ensure that the plugin for \''+format+'\' support is added');

        /*
         * need to test if it's more efficent to convert all binary strings
         * to TypedArray - or should we just leave and process as string?
         */
        if(this.supportsArrayBuffer()) {
            dataAsBinaryString = imageData;
            imageData = this.binaryStringToUint8Array(imageData);
        }

        var imageIndex = getImageIndex(images),
            info = checkImagesForAlias(dataAsBinaryString || imageData, images);

        if(!info)
            info = this['process' + format.toUpperCase()](imageData, imageIndex, alias, compression, dataAsBinaryString);

        if(!info)
            throw new Error('An unkwown error occurred whilst processing the image');

        writeImageToPDF.call(this, x, y, w, h, info, imageIndex, images);

        return this
    };

    /**
     * JPEG SUPPORT
     **/

        //takes a string imgData containing the raw bytes of
        //a jpeg image and returns [width, height]
        //Algorithm from: http://www.64lines.com/jpeg-width-height
    var getJpegSize = function(imgData) {
            'use strict'
            var width, height, numcomponents;
            // Verify we have a valid jpeg header 0xff,0xd8,0xff,0xe0,?,?,'J','F','I','F',0x00
            if (!imgData.charCodeAt(0) === 0xff ||
                !imgData.charCodeAt(1) === 0xd8 ||
                !imgData.charCodeAt(2) === 0xff ||
                !imgData.charCodeAt(3) === 0xe0 ||
                !imgData.charCodeAt(6) === 'J'.charCodeAt(0) ||
                !imgData.charCodeAt(7) === 'F'.charCodeAt(0) ||
                !imgData.charCodeAt(8) === 'I'.charCodeAt(0) ||
                !imgData.charCodeAt(9) === 'F'.charCodeAt(0) ||
                !imgData.charCodeAt(10) === 0x00) {
                throw new Error('getJpegSize requires a binary string jpeg file')
            }
            var blockLength = imgData.charCodeAt(4)*256 + imgData.charCodeAt(5);
            var i = 4, len = imgData.length;
            while ( i < len ) {
                i += blockLength;
                if (imgData.charCodeAt(i) !== 0xff) {
                    throw new Error('getJpegSize could not find the size of the image');
                }
                if (imgData.charCodeAt(i+1) === 0xc0 || //(SOF) Huffman  - Baseline DCT
                    imgData.charCodeAt(i+1) === 0xc1 || //(SOF) Huffman  - Extended sequential DCT
                    imgData.charCodeAt(i+1) === 0xc2 || // Progressive DCT (SOF2)
                    imgData.charCodeAt(i+1) === 0xc3 || // Spatial (sequential) lossless (SOF3)
                    imgData.charCodeAt(i+1) === 0xc4 || // Differential sequential DCT (SOF5)
                    imgData.charCodeAt(i+1) === 0xc5 || // Differential progressive DCT (SOF6)
                    imgData.charCodeAt(i+1) === 0xc6 || // Differential spatial (SOF7)
                    imgData.charCodeAt(i+1) === 0xc7) {
                    height = imgData.charCodeAt(i+5)*256 + imgData.charCodeAt(i+6);
                    width = imgData.charCodeAt(i+7)*256 + imgData.charCodeAt(i+8);
                    numcomponents = imgData.charCodeAt(i+9);
                    return [width, height, numcomponents];
                } else {
                    i += 2;
                    blockLength = imgData.charCodeAt(i)*256 + imgData.charCodeAt(i+1)
                }
            }
        }
        , getJpegSizeFromBytes = function(data) {

            var hdr = (data[0] << 8) | data[1];

            if(hdr !== 0xFFD8)
                throw new Error('Supplied data is not a JPEG');

            var len = data.length,
                block = (data[4] << 8) + data[5],
                pos = 4,
                bytes, width, height, numcomponents;

            while(pos < len) {
                pos += block;
                bytes = readBytes(data, pos);
                block = (bytes[2] << 8) + bytes[3];
                if((bytes[1] === 0xC0 || bytes[1] === 0xC2) && bytes[0] === 0xFF && block > 7) {
                    bytes = readBytes(data, pos + 5);
                    width = (bytes[2] << 8) + bytes[3];
                    height = (bytes[0] << 8) + bytes[1];
                    numcomponents = bytes[4];
                    return {width:width, height:height, numcomponents: numcomponents};
                }

                pos+=2;
            }

            throw new Error('getJpegSizeFromBytes could not find the size of the image');
        }
        , readBytes = function(data, offset) {
            return data.subarray(offset, offset+ 5);
        };

    jsPDFAPI.processJPEG = function(data, index, alias, compression, dataAsBinaryString) {
        'use strict'
        var colorSpace = this.color_spaces.DEVICE_RGB,
            filter = this.decode.DCT_DECODE,
            bpc = 8,
            dims;

        if(this.isString(data)) {
            dims = getJpegSize(data);
            return this.createImageInfo(data, dims[0], dims[1], dims[3] == 1 ? this.color_spaces.DEVICE_GRAY:colorSpace, bpc, filter, index, alias);
        }

        if(this.isArrayBuffer(data))
            data = new Uint8Array(data);

        if(this.isArrayBufferView(data)) {

            dims = getJpegSizeFromBytes(data);

            // if we already have a stored binary string rep use that
            data = dataAsBinaryString || this.arrayBufferToBinaryString(data);

            return this.createImageInfo(data, dims.width, dims.height, dims.numcomponents == 1 ? this.color_spaces.DEVICE_GRAY:colorSpace, bpc, filter, index, alias);
        }

        return null;
    };

    jsPDFAPI.processJPG = function(/*data, index, alias, compression, dataAsBinaryString*/) {
        return this.processJPEG.apply(this, arguments);
    }

})(jsPDF.API);
